﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace PdfLibrary
{
    public class PdfFileChunk
    {
        public int XrefTableOffset;
        public PdfXrefTable XrefTable;
        public IPdfTrailer Trailer;
        public List<PdfIndirectObject> Objects = new List<PdfIndirectObject>();

        public PdfFileChunk(PdfReader reader)
        {
            var tmpObj = new PdfIndirectObject();

            while (tmpObj.LoadFromReader(reader, false))
            {
                reader.PositionCheck("reading PdfIndirectObjects");
                ExtensionMethods.SetListItemForIndex(Objects, tmpObj.objectNumber, tmpObj);
                tmpObj = new PdfIndirectObject();
            }
            reader.PositionCheckReset();

            this.XrefTable = new PdfXrefTable();
            this.XrefTable.LoadFromReader(reader);

            this.ReadTrailer(reader);
        }

        protected void ReadTrailer(PdfReader reader)
        {
            if (reader.ReadLine() != "trailer")
                throw new PdfParsingException(reader, "trailer not found");

            this.Trailer = DictionaryObjectInterceptor.CreateProxy<IPdfTrailer>(new PdfDictionary(reader));

            if (reader.ReadLine() != "startxref")
                throw new PdfParsingException(reader, "startxref not found!");

            try { this.XrefTableOffset = reader.ReadInt(); }
            catch (Exception exc) { throw new PdfParsingException(reader, "xrefTableOffset is corrupt", exc); }

            if (reader.ReadLine() != "%%EOF")
                throw new PdfParsingException(reader, "%%EOF not found");
        }
    }

    public class PdfFile : IPdfIndirectReferenceResolver
    {
        public List<PdfFileChunk> Chunks = new List<PdfFileChunk>();
        public List<PdfIndirectObject> Objects = new List<PdfIndirectObject>();
        public Dictionary<int, PdfXrefEntry> ObjectXrefEntries = new Dictionary<int, PdfXrefEntry>();

        private readonly byte[] pdfContent;

        public int XrefTableOffset;
        public PdfXrefTable XrefTable;
        public IPdfTrailer Trailer;

        private PdfReader CreateReader()
        {
            return new PdfReader(pdfContent, this);
        }

        public PdfFile(string filename)
        {
            pdfContent = File.ReadAllBytes(filename);

            var reader = CreateReader();

            ReadLastTrailer(reader);
            
            reader.Position = 0;
            reader.ParseDirection = FileReaderParseDirection.Forward;

            // version
            reader.ReadLine();
            // comment
            reader.ReadLine();

            while (!reader.EndOfFile)
            {
                var chunk = new PdfFileChunk(reader);
                
                // set object numbers
                foreach (var obj in chunk.Objects.Where(obj => obj != null))
                    ExtensionMethods.SetListItemForIndex(Objects, obj.objectNumber, obj);
                AddXrefTable(chunk.XrefTable);

                Chunks.Add(chunk);
                reader.SkipWhitespaceCharacters();
            }
        }

        private void AddXrefTable(PdfXrefTable xrefTable)
        {
            // flattens xrefArray and selects entries with the largest generation numbers
            foreach (var xrefEntry in xrefTable.Sections.SelectMany(s => s.Subsections).SelectMany(s => s.Entries).
                    GroupBy(x => x.ObjectNumber).Select(g => g.OrderByDescending(x => x.GenerationNumber).FirstOrDefault()))
                if (!ObjectXrefEntries.ContainsKey(xrefEntry.ObjectNumber))
                    ObjectXrefEntries.Add(xrefEntry.ObjectNumber, xrefEntry);            
        }

        private void ReadLastTrailer(PdfReader reader)
        {
            reader.ParseDirection = FileReaderParseDirection.Backward;
            reader.SeekToEnd();

            if (reader.ReadLine() != "%%EOF")
                throw new PdfParsingException(reader, "%%EOF not found");

            try { this.XrefTableOffset = reader.ReadInt(); }
            catch (Exception exc) { throw new PdfParsingException(reader, "xrefTableOffset is corrupt", exc); }

            if (reader.ReadLine() != "startxref")
                throw new PdfParsingException(reader, "startxref not found!");

            reader.ParseDirection = FileReaderParseDirection.Forward;
            reader.Position = this.XrefTableOffset;

            XrefTable = new PdfXrefTable();
            XrefTable.LoadFromReader(reader);
            AddXrefTable(XrefTable);

            if (reader.ReadLine() != "trailer")
                throw new PdfParsingException(reader, "trailer not found");

            this.Trailer = DictionaryObjectInterceptor.CreateProxy<IPdfTrailer>(new PdfDictionary(reader));
        }

        public IPdfObject Resolve(PdfIndirectReference reference)
        {
            var objNumber = reference.objectNumber;

            if (objNumber >= this.Objects.Count)
            {
                var reader = CreateReader();
                reader.Position = ObjectXrefEntries[objNumber].ByteOffset;
                                
                var newObj = new PdfIndirectObject();
                newObj.LoadFromReader(reader, true);

                ExtensionMethods.SetListItemForIndex(Objects, newObj.objectNumber, newObj);
            }

            var obj = ExtensionMethods.GetListItemByIndex(Objects, objNumber);
            if (obj == null)
                throw new Exception(String.Format("Object not found! Object number: {0}", objNumber));
            return obj.content;
        }
    }
}
